package com.soos.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.soos.dto.Result;
import com.soos.entity.Shop;
import com.soos.mapper.ShopMapper;
import com.soos.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.soos.utils.CacheClient;
import com.soos.utils.RedisData;
import com.soos.utils.SystemConstants;
import jdk.nashorn.internal.ir.CallNode;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.time.LocalDateTime;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.soos.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    /**
     * 1. 实现给商品添加缓存
     * 2. 实现给查询店铺 的缓存添加超时剔除和主动更新的策略
     * 3. 解决缓存穿透问题(缓存空对象 + 布隆过滤器)
     * 4. 解决缓存雪崩问题(增加不同的/随机的TTL)
     * 5. 解决缓存击穿问题(互斥锁 + 逻辑过期)
     * 6. 封装一个缓存工具类
     */
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private final ExecutorService REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    @Resource
    private CacheClient cacheClient;

    @Override
    public Result queryShopById(Long id) {
        //1. 实现给商品添加缓存
        //return addCache(id);
        //2. 实现给查询店铺 的缓存添加超时剔除和主动更新的策略
        //Result result = activeRenewalStrategy(id);
        //3.通过缓存工具类 : 缓存空对象来解决缓存穿透问题
        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.SECONDS);
        //Shop shop1 = cachePenetration(id);
        //4.缓存击穿问题-互斥锁解决
        //Shop shop = cacheBreakdownWithMutex(id);
        //5.通过缓存工具类 : 缓存击穿-逻辑过期时间解决
        //Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.SECONDS);
        //Shop shop1 = cacheBreakdownWithLogicalExpire(id);
        if(shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    /**
     * 1. 实现给商品添加缓存
     * @param id 商铺id
     * @return 返回商铺信息
     */
    private Result addCache(Long id) {
        //给商铺添加缓存
        //2.先去查询缓存
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //3.缓存中有,直接返回
        if(shopJson != null) {
            //返回店铺信息
            return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
        }
        //4.缓存中没有,查询数据库
        Shop shop = getById(id);
        if(shop == null) {
            //5.数据库中没有,返回null
            return Result.fail("店铺不存在");
        }
        //6.数据库中有,回写redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
        return Result.ok(shop);
    }

    /**
     * 2. 实现给查询店铺 的缓存添加超时剔除和主动更新的策略
     * 用于解决缓存与数据库之间的一致性
     * 读请求 :
     *    1.如果缓存命中,直接返回
     *    2.缓存未命中,查询数据库,回写缓存,设置TTL
     * 写请求 :
     *    1.先更新数据库,后删除缓存
     *    2.保证更新数据库和删除缓存的原子性
     *    3.使用超实剔除作为兜底方案
     * @param id 商铺id
     * @return 返回商铺信息
     */
    private Result activeRenewalStrategy(Long id) {
        //2.先去查询缓存
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //3.缓存中有,直接返回
        if(shopJson != null) {
            //返回店铺信息
            return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
        }
        //4.缓存中没有,查询数据库
        Shop shop = getById(id);
        if(shop == null) {
            //5.数据库中没有,返回null
            return Result.fail("店铺不存在");
        }
        //6.数据库中有,回写redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

    /**
     * 写请求 :
     *    1.先更新数据库,后删除缓存
     *    2.保证更新数据库和删除缓存的原子性
     *    3.使用超实剔除作为兜底方案
     * @param shop 商铺信息
     * @return 返回是否更新成功
     */
    @Override
    @Transactional
    public Result updateShop(Shop shop) {
        //1.先更新数据库
        Long id = shop.getId();
        updateById(shop);
        //2.在删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }

    @Override
    public Result queryShopByType(Integer typeId, Integer current,
                                  Double x, Double y) {
        //不按照分页查询
        if(x == null || y == null) {
            // 根据类型分页查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }
        //按照地理坐标分页查询
        //1.计算分页参数
        int from = (current-1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
        //2.从redis的GEO中获取数据
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> search = stringRedisTemplate.opsForGeo().
                search(key, GeoReference.fromCoordinate(new Point(x, y)),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));
        if(search == null) {
            return Result.ok(Collections.emptyList());
        }
        List< GeoResult<RedisGeoCommands.GeoLocation<String>> > content = search.getContent();
        if(from>= content.size()) {
            return Result.ok(Collections.emptyList());
        }
        //3.根据分页参数去截取数据
        //4.解析出shopID,及其地理坐标
        List<Long> ids = new ArrayList<>(content.size());
        Map<String,Distance> distanceMap = new HashMap<>(content.size());
        content.stream().skip(from).forEach(geoResult->{
            String idStr = geoResult.getContent().getName();
            ids.add(Long.valueOf(idStr));
            Distance distance = geoResult.getDistance();
            distanceMap.put(idStr, distance);
        });

        String join = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids)
                .last("ORDER BY FIELD(id," + join + ")").list();

        for (Shop shop : shops) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        //5.封装成Shop返回
        return Result.ok(shops);
    }

    /**
     * 3.解决缓存穿透问题(cachePenetration缓存空对象 + 布隆过滤器)
     * 1.通过缓存空对象来解决缓存穿透问题
     */
    public Shop cachePenetration(Long id) {
        //2.查询redis
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //如果传入的字符串去除两端空格后的长度大于 0，返回 true
        if(StrUtil.isNotBlank(shopJson)) {
            //3.redis中有直接返回
            return JSONUtil.toBean(shopJson,Shop.class);
        }
        //4.redis中有空对象,返回null
        if(shopJson!=null) {
            return null;
        }
        //5.redis中没有,查询数据库
        Shop shop = getById(id);
        if(shop == null) {
            //6.数据库中没有,缓存空对象,返回null
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        //7.数据库中有,写入redis,返回商铺信息
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.SECONDS);
        return shop;
    }

    /**
     * 尝试获取锁
     */
    private boolean tryLock(String key) {
        Boolean isSuccess = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(isSuccess);
    }
    /**
     * 释放锁
     * @param key
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

    /**
     * 4.解决缓存击穿问题(cacheBreakdown互斥锁 + 逻辑过期)
     * 缓存击穿问题也叫作热点key问题,当部分key过去这个key被高并发访问并且缓存重建时间比较久
     * 所有请求就会全部打到数据库,给数据库带来了巨大的压力->通过互斥锁解决
     * 通过互斥锁解决缓存击穿问题cacheBreakdownWithMutex
     */
    public Shop cacheBreakdownWithMutex(Long id) {
        //2.查询redis
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //如果传入的字符串去除两端空格后的长度大于 0，返回 true
        if(StrUtil.isNotBlank(shopJson)) {
            //3.redis中有直接返回
            return JSONUtil.toBean(shopJson,Shop.class);
        }
        //4.redis中有空对象,返回null
        if(shopJson!=null) {
            return null;
        }
        String lockKey = LOCK_SHOP_KEY + id;
        Shop shop;
        //5.先要尝试获取锁
        //5.1获取锁失败
        try {
            if(!tryLock(lockKey)) {
                Thread.sleep(50);
                return cacheBreakdownWithMutex(id);
            }
            //5.2获取锁成功,并且做双重校验
            shopJson = stringRedisTemplate.opsForValue().get(key);
            //如果传入的字符串去除两端空格后的长度大于 0，返回 true
            if(StrUtil.isNotBlank(shopJson)) {
                //3.redis中有直接返回
                return JSONUtil.toBean(shopJson,Shop.class);
            }
            //6.redis中没有,缓存重建,查询数据库
            //模拟重建延迟
            Thread.sleep(200);
            shop  = getById(id);
            if(shop == null) {
                //7.数据库中没有,缓存空对象,返回null
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //7.数据库中有,写入redis,返回商铺信息
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //8.别忘了释放锁
            unlock(lockKey);
        }
        return shop;
    }

    /**
     * 4.解决缓存击穿问题(cacheBreakdown互斥锁 + 逻辑过期)
     * 通过逻辑过期解决缓存击穿问题
     *    先要把热点key加载到redis中(缓存预热) + 并且要加上逻辑过期时间
     */
    public Shop cacheBreakdownWithLogicalExpire(Long id) {
        //2.先获取缓存中的RedisData
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject jsonObject = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(jsonObject, Shop.class);
        //3.判断商品信息是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if(expireTime.isAfter(LocalDateTime.now())) {
            //4.没有过期,直接返回
            return shop;
        }
        //4.过期,先要获取锁信息
        String lockKey = LOCK_SHOP_KEY + id;
        if(tryLock(lockKey)) {
            //6.获取锁成功,做double check
            if (expireTime.isAfter(LocalDateTime.now())) {
                //7.没有过期,直接返回
                return shop;
            }
            //8.另外开启一个线程做缓存重建
            REBUILD_EXECUTOR.submit(() -> {
                try {
                    shopToRedis(id, 20L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    //9.释放锁
                    unlock(lockKey);
                }
            });
        }
        //返回店铺信息,获取锁失败,直接返回旧的数据
        return shop;
    }
    //缓存重建
    public void shopToRedis(Long id,Long expireTime) throws InterruptedException {
        //测试模拟
        Shop shop = getById(id);
        Thread.sleep(200);
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
        redisData.setData(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }
}